package jp.wug.core;

import com.google.inject.TypeLiteral;

import java.io.IOException;
import java.io.Writer;
import java.util.*;

class WritableContext {
    private final ControllerFactory.Controller controller;
    private final Page page;

    private final Model model;
    private final TypedStack typedStack;
    private final List<String> parentPathArgs;
    private final List<String> pathArgs;
    private final String pathString;
    private final JsonTree jsonTree;

    private static class TypedStack {
        private final TypedStack parent;
        private final TypeLiteral<?> type;
        private final Object value;

        TypedStack() {
            this(null, null, null);
        }

        TypedStack(TypedStack parent, TypeLiteral<?> type, Object value) {
            this.parent = parent;
            this.type = type;
            this.value = value;
        }

        <T> TypedStack push(TypeLiteral<T> type, T t) {
            return new TypedStack(this, type, t);
        }

        @SuppressWarnings("unchecked")
        <T> T get(TypeLiteral<T> type) {
            if (parent == null) {
                throw new NoSuchElementException();
            }
            if (this.type.equals(type)) {
                return (T) value;
            }
            return parent.get(type);
        }
    }

    private static class JsonTree {
        Map<String, JsonTree> children = new TreeMap<String, JsonTree>();
        Map<String, String> strings = new TreeMap<String, String>();

        JsonTree getChild(String name) {
            JsonTree child = children.get(name);
            if (child == null) {
                assert !strings.containsKey(name);
                child = new JsonTree();
                children.put(name, child);
            }
            return child;
        }

        private JsonTree prepare(List<String> list) {
            if (list.size() > 1) {
                JsonTree child = getChild(list.get(0));
                return child.prepare(list.subList(1, list.size()));
            }
            return this;
        }

        private String tail(List<String> list) {
            return list.get(list.size() - 1);
        }

        JsonTree getChild(List<String> list) {
            return prepare(list).getChild(tail(list));
        }

        private void put0(String name, String value) {
            assert !children.containsKey(name);
            strings.put(name, value);
        }

        void putString(List<String> list, String value) {
            String escapedValue;
            if (value == null) {
                escapedValue = "null";
            } else {
                escapedValue = "\"" + value.replace("\"", "\\\"") + "\"";
            }
            prepare(list).put0(tail(list), escapedValue);
        }

        void putBoolean(List<String> list, Boolean value) {
            String escapedValue;
            if (value == null) {
                escapedValue = "null";
            } else {
                escapedValue = value.toString();
            }
            prepare(list).put0(tail(list), escapedValue);
        }

        void write(Writer writer) throws IOException {
            writer.write("{");
            boolean needsComma = false;
            for (Map.Entry<String, String> entry : strings.entrySet()) {
                if (needsComma) {
                    writer.write(",");
                } else {
                    needsComma = true;
                }
                writer.write("\"");
                writer.write(entry.getKey());
                writer.write("\":");
                writer.write(entry.getValue());
            }
            for (Map.Entry<String, JsonTree> entry : children.entrySet()) {
                if (needsComma) {
                    writer.write(",");
                } else {
                    needsComma = true;
                }
                writer.write("\"");
                writer.write(entry.getKey());
                writer.write("\":");
                entry.getValue().write(writer);
            }
            writer.write("}");
        }
    }

    WritableContext(Page page, Model model, ControllerFactory.Controller controller) {
        this.page = page;
        this.controller = controller;
        this.model = model;
        parentPathArgs = Collections.emptyList();
        pathArgs = Collections.emptyList();
        pathString = "";
        typedStack = new TypedStack();
        jsonTree = new JsonTree();
    }

    private WritableContext(WritableContext context, TypedStack typedStack) {
        this(context, context.model, typedStack, context.jsonTree, context.pathArgs, context.pathString, context.parentPathArgs);
    }

    private WritableContext(WritableContext context, Model model, JsonTree jsonTree) {
        this(context, model, context.typedStack, jsonTree, context.pathArgs, context.pathString, context.parentPathArgs);
    }

//    private WritableContext(WritableContext context, List<String> pathArgs, String pathString, List<String> parentPathArgs, JsonTree jsonTree) {
//        this(context, context.model, context.typedStack, jsonTree, pathArgs, pathString, parentPathArgs);
//    }

    private WritableContext(WritableContext context, Model model, TypedStack typedStack, JsonTree jsonTree, List<String> pathArgs, String pathString, List<String> parentPathArgs) {
        page = context.page;
        controller = context.controller;
        this.model = model;
        this.parentPathArgs = parentPathArgs;
        this.pathArgs = pathArgs;
        this.pathString = pathString;
        this.typedStack = typedStack;
        this.jsonTree = jsonTree;
    }

    String getString(Expression<Void, String> expression) {
        String s = call(expression);
        jsonTree.putString(expression.asStrings(), s);
        return s;
    }

    Boolean getBoolean(Expression<Void, Boolean> expression) {
        Boolean b = call(expression);
        jsonTree.putBoolean(expression.asStrings(), b);
        return b;
    }

    <T> T call(Expression<Void, T> expression) {
        return call(expression, null);
    }

    <T, U> T call(Expression<U, T> expression, U u) {
        return expression.call(model, u);
    }

    WritableContext putModel(Expression<Void, Model> expression) {
        return putModel(expression, call(expression));
    }

    WritableContext putModel(Expression<?, ?> expression, Model model) {
        return new WritableContext(this, model, jsonTree.getChild(expression.asStrings()));
    }

    WritableContext putCatch(Model model) {
        return new WritableContext(this, model, new JsonTree());
    }

    WritableContext putModelWithArg(Model model, String arg, Expression<?, ?> expression) {
        List<String> newPathArgs = new ArrayList<String>();
        newPathArgs.addAll(pathArgs);
        newPathArgs.add(arg);

        JsonTree t = jsonTree.getChild(expression.asStrings()).getChild(arg);

        return new WritableContext(this, model, typedStack, t, newPathArgs, pathString, parentPathArgs);
    }

    WritableContext putModelWithUniqueArg(Model model, String key, String arg, Expression<?, ?> expression) {
        List<String> newPathArgs = new ArrayList<String>();
        newPathArgs.addAll(pathArgs);
        newPathArgs.add(arg);

        JsonTree t = jsonTree.getChild(expression.asStrings()).getChild(arg);

        WritableContext child = new WritableContext(this, model, typedStack, t, newPathArgs, pathString, parentPathArgs);
        return new WritableContext(this, model, typedStack, t, Collections.<String>emptyList(), child.getNameWithPathArgs(key) + ":", child.getFullPathArgs());
    }

    List<ModelException> getExceptions(TypeLiteral<?> type, String key) {
        List<ModelException> list = new ArrayList<ModelException>();
        for (ModelException modelException : controller.getExceptions(type, getNameWithPathArgs(key))) {
            list.add(modelException);
        }
        return list;
    }

    List<String> getPathArgs() {
        return Collections.unmodifiableList(pathArgs);
    }

    List<String> getPathArgs(int depth) {
        return pathArgs.subList(pathArgs.size() - depth, pathArgs.size());
    }

    private List<String> getFullPathArgs() {
        List<String> list = new ArrayList<String>();
        list.addAll(parentPathArgs);
        list.addAll(pathArgs);
        return list;
    }

    private String join(String name, List<String> args, String separator) {
        if (name.contains(":")) {
            throw new RuntimeException("FIXME: key contains ':': " + name);
        }
        for (String s : args) {
            if (s.contains(":")) {
                throw new RuntimeException("FIXME: arg contains ':': " + s);
            }
        }
        StringBuffer buffer = new StringBuffer();
        buffer.append(name);
        for (String s : args) {
            buffer.append(separator);
            buffer.append(s);
        }
        return buffer.toString();
    }

    private String toPathString(String name, List<String> args) {
        return join(name, args, ":");
    }

    String getNameWithPathArgs(String name) {
        return pathString + toPathString(name, getPathArgs());
    }

    String getNameForFormControl(String name, int depth) {
        return toPathString(name, getPathArgs(depth));
    }

    String getActionURL(String name) {
        return page.toPathInfoPlusQueryParameterForActionByModel(getNameWithPathArgs(name));
    }

    String getPostParameter(String name) {
        return getNameWithPathArgs(name);
    }

    String getPostURL() {
        return page.toPathInfoPlusQueryParameterForFormByModel();
    }

    String getLinkURL(PageFactory.Template template, Expression<Void, Model> expression) {
        Model linkModel = expression.call(model, null);
        Page linkPage = template.create(linkModel.getValue());
        return linkPage.toPathInfoPlusQueryParameterByModel();
    }

    void writeExported(Writer writer, String key, PageFactory.Template template) throws IOException {
        Page page = template.create(model.getValue());
        WritableContext child = new WritableContext(this, page.model, typedStack, jsonTree.getChild("_import").getChild(key), Collections.<String>emptyList(), getNameWithPathArgs(key) + ":", getFullPathArgs());
        page.writeExported(writer, child);
    }

    <T> T get(TypeLiteral<T> type) {
        return typedStack.get(type);
    }

    <T> WritableContext push(TypeLiteral<T> type, T t) {
        return new WritableContext(this, typedStack.push(type, t));
    }

    void dump(Writer writer) throws IOException {
        jsonTree.write(writer);
    }
}